package com.song.uaa.authserver.config;

import java.security.Principal;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.MediaType;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.core.OAuth2AccessToken;
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.StandardClaimNames;
import org.springframework.security.oauth2.server.authorization.OAuth2Authorization;
import org.springframework.security.oauth2.server.authorization.OAuth2TokenType;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.util.matcher.MediaTypeRequestMatcher;

@Configuration
public class SecurityConfig {
	
	@Bean 
	@Order(Ordered.HIGHEST_PRECEDENCE)
	SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
			throws Exception {
		OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
		http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
			.oidc(oidc -> 
				// 拉取用户信息时，映射权限到声明属性中
				oidc.userInfoEndpoint(user -> user.userInfoMapper(userInfoMapper()))
			);	// Enable OpenID Connect 1.0
		http
			// Redirect to the login page when not authenticated from the
			// authorization endpoint
			.exceptionHandling((exceptions) -> exceptions
				.defaultAuthenticationEntryPointFor(
					new LoginUrlAuthenticationEntryPoint("/login"),
					new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
				)
			)
			// Accept access tokens for User Info and/or Client Registration
			.oauth2ResourceServer((resourceServer) -> resourceServer
					.jwt(Customizer.withDefaults()));
//			.oauth2ResourceServer((resourceServer) -> resourceServer
//				.opaqueToken(Customizer.withDefaults()));

		
		// 开启cors, 放行前端地址
		http.cors(Customizer.withDefaults());
		return http.build();
	}

	@Bean 
	@Order(2)
	SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
			throws Exception {
		http
			.authorizeHttpRequests((authorize) -> authorize
				.anyRequest().authenticated()
			)
			// Form login handles the redirect to the login page from the
			// authorization server filter chain
			.formLogin(Customizer.withDefaults());
		
		return http.build();
	}
	
	// @formatter:off
	private static final List<String> EMAIL_CLAIMS = Arrays.asList(
			StandardClaimNames.EMAIL,
			StandardClaimNames.EMAIL_VERIFIED
	);
	private static final List<String> PHONE_CLAIMS = Arrays.asList(
			StandardClaimNames.PHONE_NUMBER,
			StandardClaimNames.PHONE_NUMBER_VERIFIED
	);
	private static final List<String> PROFILE_CLAIMS = Arrays.asList(
			StandardClaimNames.NAME,
			StandardClaimNames.FAMILY_NAME,
			StandardClaimNames.GIVEN_NAME,
			StandardClaimNames.MIDDLE_NAME,
			StandardClaimNames.NICKNAME,
			StandardClaimNames.PREFERRED_USERNAME,
			StandardClaimNames.PROFILE,
			StandardClaimNames.PICTURE,
			StandardClaimNames.WEBSITE,
			StandardClaimNames.GENDER,
			StandardClaimNames.BIRTHDATE,
			StandardClaimNames.ZONEINFO,
			StandardClaimNames.LOCALE,
			StandardClaimNames.UPDATED_AT
	);
	// @formatter:on
			
	/**
	 * 在获取访问令牌后，自动请求用户端点时应用该Bean。
	 * 当前网关客户端中，未使用访问令牌中的声明，而是使用访问令牌的断点
	 * 
	 * @see org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationProvider.userInfoMapper
	 * {@link https://docs.spring.io/spring-authorization-server/reference/protocol-endpoints.html#oidc-user-info-endpoint}
	 */
	Function<OidcUserInfoAuthenticationContext, OidcUserInfo> userInfoMapper() {
		return authenticationContext -> {
			OAuth2Authorization authorization = authenticationContext.getAuthorization();
			OAuth2AccessToken accessToken = authenticationContext.getAccessToken();
			Map<String, Object> scopeRequestedClaims = getClaimsRequestedByScope(authorization,
					accessToken.getScopes());
			return new OidcUserInfo(scopeRequestedClaims);
		};
	}
	
	private static Map<String, Object> getClaimsRequestedByScope(OAuth2Authorization authorization,
			Set<String> requestedScopes) {
		Map<String, Object> claims = authorization.getToken(OidcIdToken.class).getToken().getClaims();
		
		Set<String> scopeRequestedClaimNames = new HashSet<>(32);
		scopeRequestedClaimNames.add(StandardClaimNames.SUB);

		if (requestedScopes.contains(OidcScopes.ADDRESS)) {
			scopeRequestedClaimNames.add(StandardClaimNames.ADDRESS);
		}
		if (requestedScopes.contains(OidcScopes.EMAIL)) {
			scopeRequestedClaimNames.addAll(EMAIL_CLAIMS);
		}
		if (requestedScopes.contains(OidcScopes.PHONE)) {
			scopeRequestedClaimNames.addAll(PHONE_CLAIMS);
		}
		if (requestedScopes.contains(OidcScopes.PROFILE)) {
			scopeRequestedClaimNames.addAll(PROFILE_CLAIMS);
		}

		Map<String, Object> requestedClaims = new HashMap<>(claims);
		requestedClaims.keySet().removeIf((claimName) -> !scopeRequestedClaimNames.contains(claimName));
		
		// 将用户权限添加到声明上
		Principal principal = authorization.getAttribute(Principal.class.getName());
		if(principal instanceof Authentication) {
			Authentication auth = (Authentication) principal;
			Set<String> roles = auth.getAuthorities().stream()
					.map(GrantedAuthority::getAuthority)
					.map(c -> c.replaceFirst("^ROLE_", ""))
					.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); 
			requestedClaims.put("roles", roles); 
		}
		return requestedClaims;
	}

	
	/**
	 * 将用户角色权限添加到访问令牌上
	 */
	@Bean
	OAuth2TokenCustomizer<JwtEncodingContext> jwtTokenCustomizer() { 
		return (context) -> {
			if (OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) { 
				context.getClaims().claims((claims) -> { 
					Set<String> roles = AuthorityUtils.authorityListToSet(context.getPrincipal().getAuthorities())
							.stream()
							.map(c -> c.replaceFirst("^ROLE_", ""))
							.collect(Collectors.collectingAndThen(Collectors.toSet(), Collections::unmodifiableSet)); 
					claims.put("roles", roles); 
				});
			}
		};
	}
}
